package hos.biometric;

/**
 * <p>Title: FingerprintDialogFragment </p>
 * <p>Description:  </p>
 * <p>Company: www.mapuni.com </p>
 *
 * @author : 蔡俊峰
 * @version : 1.0
 * @date : 2022/11/18 10:21
 */

import android.app.Dialog;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProvider;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * A fragment that provides a standard prompt UI for fingerprint authentication on versions prior
 * to Android 9.0 (API 28).
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class FingerprintDialogFragment extends DialogFragment {
 private static final String TAG = "FingerprintFragment";

 /**
  * The dialog has not been initialized.
  */
 static final int STATE_NONE = 0;

 /**
  * Waiting for the user to authenticate with fingerprint.
  */
 static final int STATE_FINGERPRINT = 1;

 /**
  * An error or failure occurred during fingerprint authentication.
  */
 static final int STATE_FINGERPRINT_ERROR = 2;

 /**
  * The user has successfully authenticated with fingerprint.
  */
 static final int STATE_FINGERPRINT_AUTHENTICATED = 3;

 /**
  * A possible state for the fingerprint dialog.
  */
 @IntDef({
         STATE_NONE,
         STATE_FINGERPRINT,
         STATE_FINGERPRINT_ERROR,
         STATE_FINGERPRINT_AUTHENTICATED
 })
 @Retention(RetentionPolicy.SOURCE)
 @interface State {}

 /**
  * Transient errors and help messages will be displayed on the dialog for this amount of time.
  */
 private static final int MESSAGE_DISPLAY_TIME_MS = 2000;

 /**
  * A handler used to post delayed events.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 final Handler mHandler = new Handler(Looper.getMainLooper());

 /**
  * A runnable that resets the dialog to its default state and appearance.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 final Runnable mResetDialogRunnable = new Runnable() {
  @Override
  public void run() {
   resetDialog();
  }
 };

 /**
  * The view model for the ongoing authentication session.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
         BiometricViewModel mViewModel;

 /**
  * The text color used for displaying error messages.
  */
 private int mErrorTextColor;

 /**
  * The text color used for displaying help messages.
  */
 private int mNormalTextColor;

 /**
  * An icon shown on the dialog during authentication.
  */
 @Nullable
 private ImageView mFingerprintIcon;

 /**
  * Help text shown below the fingerprint icon on the dialog.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 @Nullable
 TextView mHelpMessageView;

 /**
  * Creates a new instance of {@link FingerprintDialogFragment}.
  *
  * @return A {@link FingerprintDialogFragment}.
  */
 @NonNull
 static FingerprintDialogFragment newInstance() {
  return new FingerprintDialogFragment();
 }

 @Override
 public void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  connectViewModel();

  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
   mErrorTextColor = getThemedColorFor(Api26Impl.getColorErrorAttr());
  } else {
   final Context context = getContext();
   mErrorTextColor = context != null
           ? ContextCompat.getColor(context, R.color.biometric_error_color)
           : 0;
  }
  mNormalTextColor = getThemedColorFor(android.R.attr.textColorSecondary);
 }

 @Override
 @NonNull
 public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
  final AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
  builder.setTitle(mViewModel.getTitle());

  // We have to use builder.getContext() instead of the usual getContext() in order to get
  // the appropriately themed context for this dialog.
  final View layout = LayoutInflater.from(builder.getContext())
          .inflate(R.layout.fingerprint_dialog_layout, null);

  final TextView subtitleView = layout.findViewById(R.id.fingerprint_subtitle);
  if (subtitleView != null) {
   final CharSequence subtitle = mViewModel.getSubtitle();
   if (TextUtils.isEmpty(subtitle)) {
    subtitleView.setVisibility(View.GONE);
   } else {
    subtitleView.setVisibility(View.VISIBLE);
    subtitleView.setText(subtitle);
   }
  }

  final TextView descriptionView = layout.findViewById(R.id.fingerprint_description);
  if (descriptionView != null) {
   final CharSequence description = mViewModel.getDescription();
   if (TextUtils.isEmpty(description)) {
    descriptionView.setVisibility(View.GONE);
   } else {
    descriptionView.setVisibility(View.VISIBLE);
    descriptionView.setText(description);
   }
  }

  mFingerprintIcon = layout.findViewById(R.id.fingerprint_icon);
  mHelpMessageView = layout.findViewById(R.id.fingerprint_error);

  final CharSequence negativeButtonText =
          AuthenticatorUtils.isDeviceCredentialAllowed(mViewModel.getAllowedAuthenticators())
                  ? getString(R.string.confirm_device_credential_password)
                  : mViewModel.getNegativeButtonText();
  builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
   @Override
   public void onClick(DialogInterface dialog, int which) {
    mViewModel.setNegativeButtonPressPending(true);
   }
  });

  builder.setView(layout);
  Dialog dialog = builder.create();
  dialog.setCanceledOnTouchOutside(false);
  return dialog;
 }

 @Override
 public void onResume() {
  super.onResume();
  mViewModel.setFingerprintDialogPreviousState(STATE_NONE);
  mViewModel.setFingerprintDialogState(STATE_FINGERPRINT);
  mViewModel.setFingerprintDialogHelpMessage(
          getString(R.string.fingerprint_dialog_touch_sensor));
 }

 @Override
 public void onPause() {
  super.onPause();
  mHandler.removeCallbacksAndMessages(null);
 }

 @Override
 public void onCancel(@NonNull DialogInterface dialog) {
  super.onCancel(dialog);
  mViewModel.setFingerprintDialogCancelPending(true);
 }

 /**
  * Connects the {@link BiometricViewModel} for the ongoing authentication session to this
  * fragment.
  */
 private void connectViewModel() {
  final FragmentActivity activity = getActivity();
  if (activity == null) {
   return;
  }
  mViewModel = new ViewModelProvider(activity.getViewModelStore(), ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication())).get(BiometricViewModel.class);

  mViewModel.getFingerprintDialogState().observe(this, new Observer<Integer>() {
   @Override
   public void onChanged(@State Integer state) {
    mHandler.removeCallbacks(mResetDialogRunnable);
    updateFingerprintIcon(state);
    updateHelpMessageColor(state);
    mHandler.postDelayed(mResetDialogRunnable, MESSAGE_DISPLAY_TIME_MS);
   }
  });

  mViewModel.getFingerprintDialogHelpMessage().observe(this, new Observer<CharSequence>() {
   @Override
   public void onChanged(CharSequence helpMessage) {
    mHandler.removeCallbacks(mResetDialogRunnable);
    updateHelpMessageText(helpMessage);
    mHandler.postDelayed(mResetDialogRunnable, MESSAGE_DISPLAY_TIME_MS);
   }
  });
 }

 /**
  * Updates the fingerprint icon to match the new dialog state, including animating between
  * states if necessary.
  *
  * @param state The new state for the fingerprint dialog.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 void updateFingerprintIcon(@State int state) {
  // May be null if we're intentionally suppressing the dialog.
  if (mFingerprintIcon == null) {
   return;
  }

  // Devices older than this do not have FP support (and also do not support SVG), so it's
  // fine for this to be a no-op. An error is returned immediately and the dialog is not
  // shown.
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
   @State final int previousState = mViewModel.getFingerprintDialogPreviousState();

   Drawable icon = getAssetForTransition(previousState, state);
   if (icon == null) {
    return;
   }

   mFingerprintIcon.setImageDrawable(icon);
   if (shouldAnimateForTransition(previousState, state)) {
    Api21Impl.startAnimation(icon);
   }

   mViewModel.setFingerprintDialogPreviousState(state);
  }
 }

 /**
  * Updates the color of the help message text to match the new dialog state.
  *
  * @param state The new state for the fingerprint dialog.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 void updateHelpMessageColor(@State int state) {
  if (mHelpMessageView != null) {
   final boolean isError = state == STATE_FINGERPRINT_ERROR;
   mHelpMessageView.setTextColor(isError ? mErrorTextColor : mNormalTextColor);
  }
 }

 /**
  * Changes the help message text shown on the dialog.
  *
  * @param helpMessage The new help message text for the dialog.
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 void updateHelpMessageText(@Nullable CharSequence helpMessage) {
  if (mHelpMessageView != null) {
   mHelpMessageView.setText(helpMessage);
  }
 }

 /**
  * Resets the appearance of the dialog to its initial state (i.e. waiting for authentication).
  */
 @SuppressWarnings("WeakerAccess") /* synthetic access */
 void resetDialog() {
  final Context context = getContext();
  if (context == null) {
   Log.w(TAG, "Not resetting the dialog. Context is null.");
   return;
  }

  mViewModel.setFingerprintDialogState(STATE_FINGERPRINT);
  mViewModel.setFingerprintDialogHelpMessage(
          context.getString(R.string.fingerprint_dialog_touch_sensor));
 }

 /**
  * Gets the theme color corresponding to a given style attribute.
  *
  * @param attr The desired attribute.
  * @return The theme color for that attribute.
  */
 private int getThemedColorFor(int attr) {
  final Context context = getContext();
  final FragmentActivity activity = getActivity();
  if (context == null || activity == null) {
   Log.w(TAG, "Unable to get themed color. Context or activity is null.");
   return 0;
  }

  TypedValue tv = new TypedValue();
  Resources.Theme theme = context.getTheme();
  theme.resolveAttribute(attr, tv, true /* resolveRefs */);
  TypedArray arr = activity.obtainStyledAttributes(tv.data, new int[] {attr});

  final int color = arr.getColor(0 /* index */, 0 /* defValue */);
  arr.recycle();
  return color;
 }

 /**
  * Checks if the fingerprint icon should animate when transitioning between dialog states.
  *
  * @param previousState The previous state for the fingerprint dialog.
  * @param state The new state for the fingerprint dialog.
  * @return Whether the fingerprint icon should animate.
  */
 private boolean shouldAnimateForTransition(@State int previousState, @State int state) {
  if (previousState == STATE_NONE && state == STATE_FINGERPRINT) {
   return false;
  } else if (previousState == STATE_FINGERPRINT && state == STATE_FINGERPRINT_ERROR) {
   return true;
  } else if (previousState == STATE_FINGERPRINT_ERROR && state == STATE_FINGERPRINT) {
   return true;
  } else if (previousState == STATE_FINGERPRINT && state == STATE_FINGERPRINT_AUTHENTICATED) {
   // TODO(b/77328470): add animation when fingerprint is authenticated
   return false;
  }
  return false;
 }

 /**
  * Gets the icon or animation asset that should appear when transitioning between dialog states.
  *
  * @param previousState The previous state for the fingerprint dialog.
  * @param state The new state for the fingerprint dialog.
  * @return A drawable asset to be used for the fingerprint icon.
  */
 private Drawable getAssetForTransition(@State int previousState, @State int state) {
  final Context context = getContext();
  if (context == null) {
   Log.w(TAG, "Unable to get asset. Context is null.");
   return null;
  }

  int iconRes;
  if (previousState == STATE_NONE && state == STATE_FINGERPRINT) {
   iconRes = R.drawable.fingerprint_dialog_fp_icon;
  } else if (previousState == STATE_FINGERPRINT && state == STATE_FINGERPRINT_ERROR) {
   iconRes = R.drawable.fingerprint_dialog_error;
  } else if (previousState == STATE_FINGERPRINT_ERROR && state == STATE_FINGERPRINT) {
   iconRes = R.drawable.fingerprint_dialog_fp_icon;
  } else if (previousState == STATE_FINGERPRINT
          && state == STATE_FINGERPRINT_AUTHENTICATED) {
   // TODO(b/77328470): add animation when fingerprint is authenticated
   iconRes = R.drawable.fingerprint_dialog_fp_icon;
  } else {
   return null;
  }

  return ContextCompat.getDrawable(context, iconRes);
 }

 /**
  * Nested class to avoid verification errors for methods introduced in Android 8.0 (API 26).
  */
 @RequiresApi(Build.VERSION_CODES.O)
 private static class Api26Impl {
  // Prevent instantiation.
  private Api26Impl() {}

  /**
   * Gets the resource ID of the {@code colorError} style attribute.
   */
  static int getColorErrorAttr() {
   return android.R.attr.colorError;
  }
 }

 /**
  * Nested class to avoid verification errors for methods introduced in Android 5.0 (API 21).
  */
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 private static class Api21Impl {
  // Prevent instantiation.
  private Api21Impl() {}

  /**
   * Starts animating the given icon if it is an {@link AnimatedVectorDrawable}.
   *
   * @param icon A {@link Drawable} icon asset.
   */
  static void startAnimation(@NonNull Drawable icon) {
   if (icon instanceof AnimatedVectorDrawable) {
    ((AnimatedVectorDrawable) icon).start();
   }
  }
 }
}
